Skip to content

Fix #13736: don't forget private property expressions of different classes#4896

Merged
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-vttjzzt
Feb 12, 2026
Merged

Fix #13736: don't forget private property expressions of different classes#4896
ondrejmirtes merged 2 commits into2.1.xfrom
create-pull-request/patch-vttjzzt

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a static or instance method is called on a base class, PHPStan was incorrectly invalidating type-narrowed private property expressions of the current class, even though the called method cannot access those properties. For example, calling self::assertTrue(true) (defined in BaseClass) would forget that self::$foo (a private property in the child class) had been narrowed from ?Foo to Foo.

Changes

  • Added optional ?ClassReflection $invalidatingClass parameter to MutatingScope::invalidateExpression() and MutatingScope::shouldInvalidateExpression() (src/Analyser/MutatingScope.php)
  • Added MutatingScope::isPrivatePropertyOfDifferentClass() helper method that checks if an expression is a private property fetch (instance or static) whose declaring class differs from the invalidating class
  • Updated both call sites in NodeScopeResolver.php to pass $methodReflection->getDeclaringClass() when invalidating expressions after method calls with side effects:
    • Instance method calls (~line 3188)
    • Static method calls (~line 3398)
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-13736.php
  • Updated CLAUDE.md with documentation about the invalidation pattern

Root cause

MutatingScope::shouldInvalidateExpression() has special logic that when $this is being invalidated, it also matches expressions containing self, static, parent, or the current class name — thereby invalidating static and instance property fetches like self::$foo and $this->foo. However, it did not consider PHP's visibility rules: a method in a parent class cannot access private properties of a child class. The fix adds a check that preserves private property expression types when the invalidating method's declaring class differs from the property's declaring class.

Test

The regression test covers:

  • Static private property: self::$foo = new Foo() followed by self::assertTrue(true) (inherited from parent) — type should remain Foo, not revert to Foo|null
  • Instance private property: $this->instanceFoo = new Foo() followed by parent::doSomething() — type should remain Foo, not revert to Foo|null

Fixes phpstan/phpstan#13736

ondrejmirtes and others added 2 commits February 12, 2026 14:30
- When a method with side effects is called, private properties that the
  method's declaring class cannot access should not be invalidated
- Added invalidatingClass parameter to invalidateExpression() and
  shouldInvalidateExpression() in MutatingScope
- Added isPrivatePropertyOfDifferentClass() to check if a property
  expression refers to a private property declared in a different class
  than the one whose method triggered the invalidation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-13736.php

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes merged commit 8a619a5 into 2.1.x Feb 12, 2026
631 of 640 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-vttjzzt branch February 12, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

don't forget private property expressions of different classes

2 participants